package gridfs.fuse;


import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.client.MongoDatabase;
import gridfs.dao.GridFile;
import gridfs.dao.GridFsAdaptor;
import gridfs.exception.BufferSizeTooLargeException;
import gridfs.exception.NotFoundException;
import gridfs.exception.NotReadyException;
import gridfs.exception.OutOfRangeException;
import gridfs.util.ScheduledUtil;
import jnr.ffi.Platform;
import jnr.ffi.Pointer;
import jnr.ffi.types.mode_t;
import jnr.ffi.types.off_t;
import jnr.ffi.types.size_t;
import ru.serce.jnrfuse.ErrorCodes;
import ru.serce.jnrfuse.FuseFillDir;
import ru.serce.jnrfuse.FuseStubFS;
import ru.serce.jnrfuse.struct.FileStat;
import ru.serce.jnrfuse.struct.FuseContext;
import ru.serce.jnrfuse.struct.FuseFileInfo;
import ru.serce.jnrfuse.struct.Statvfs;

import java.io.UnsupportedEncodingException;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static jnr.ffi.Platform.OS.WINDOWS;

public class GridFS extends FuseStubFS {

    static GridFsAdaptor adaptor;

    public static Map<Long, List<TokenBucket>> pidToBucketList = new ConcurrentHashMap<Long, List<TokenBucket>>();
    
    static List<TokenBucket> defaultBucket = new ArrayList<TokenBucket>(){{
        this.add(new TokenBucket("public","/public"));
    }};

    public static void main(String[] args) throws Exception {
        GridFS memfs = new GridFS();
        try {
            String path;
            GridFS.setUpDB();
            switch (Platform.getNativePlatform().getOS()) {
                case WINDOWS:
                    path = "R:\\";
                    break;
                default:
                    path = "/tmp/resource";
            }
            ScheduledUtil.start();
            memfs.mount(Paths.get(path), true, false);
        } finally {
            memfs.umount();
            adaptor.close();
            ScheduledUtil.executorService.shutdown();
        }
    }

    public static void setUpDB() throws Exception {
        MongoClient mongo = new MongoClient(new MongoClientURI("mongodb://127.0.0.1:27017/"));
        MongoDatabase database = mongo.getDatabase("bsci");
        adaptor = GridFsAdaptor.newBuilder()
                .database(database)
                .build();
    }


    @Override
    public int getattr(String path, FileStat stat) {
        // 不停的调用, 有点像ping

        FuseContext ctx = getContext();
        stat.st_uid.set(ctx.uid.get());
        stat.st_gid.set(ctx.gid.get());
        if("/".equals(path)){
            stat.st_mode.set(FileStat.S_IFDIR | 0444);
            stat.st_size.set(0);
            stat.st_nlink.set(2);
            return 0;
        }
        if("/config.properties".equals(path)){
            stat.st_mode.set(FileStat.S_IFREG | 0666);
            stat.st_size.set(0);
            stat.st_nlink.set(1);
            return 0;
        }
        TokenBucket token = getToken(path);
        if(token == null) return -ErrorCodes.ENOENT();
        if(token.context.equals(path)){
            stat.st_mode.set(FileStat.S_IFDIR | 0777);
            stat.st_size.set(0);
            stat.st_nlink.set(2);
            return 0;
        }
        GridFile file = adaptor.get(path, token.token);
        if (file == null) return -ErrorCodes.ENOENT();
        if(file.getMetadata().get("isFile") != null) {
            stat.st_mode.set(FileStat.S_IFREG | 0777);
            stat.st_nlink.set(1);
        } else {
            stat.st_mode.set(FileStat.S_IFDIR | 0777);
            stat.st_nlink.set(2);
        }
        stat.st_size.set(file.getSize());
        return 0;
    }

    @Override
    public int create(String path, @mode_t long mode, FuseFileInfo fi) {
        TokenBucket token = getToken(path);
        if(token == null) return -ErrorCodes.ENOENT();
        if (adaptor.get(path, token.token) != null) {
            return -ErrorCodes.EEXIST();
        }
        String dirname = path.substring(0,path.lastIndexOf('/'));
        if (token.context.equals(dirname) ||
                adaptor.get(dirname, token.token).getMetadata().get("isDir") != null) {
            adaptor.create(path, dirname, token.token);
            return 0;
        }
        return -ErrorCodes.ENOENT();
    }


    @Override
    public int mkdir(String path, @mode_t long mode) {
        TokenBucket token = getToken(path);
        if(token == null) return -ErrorCodes.ENOENT();
        if (adaptor.get(path, token.token) != null) {
            return -ErrorCodes.EEXIST();
        }
        String dirname = path.substring(0,path.lastIndexOf('/'));
        if (token.context.equals(dirname) ||
                adaptor.get(dirname, token.token).getMetadata().get("isDir") != null) {
            adaptor.mkdir(path, dirname, token.token);
            return 0;
        }
        return -ErrorCodes.ENOENT();
    }


    private TokenBucket getToken(String path) {
        FuseContext ctx = getContext();
        List<TokenBucket> bucketList = Optional.ofNullable(pidToBucketList.get(ctx.pid.get())).orElse(defaultBucket);
        for(TokenBucket b : bucketList){
            if(path.startsWith(b.context)) return b;
        }
        return null;
    }


    @Override
    public int open(String path, FuseFileInfo fi) {
        return 0;
    }

    @Override
    public int read(String path, Pointer buf, @size_t long size, @off_t long offset, FuseFileInfo fi) {
        TokenBucket token = getToken(path);
        if(token == null) return -ErrorCodes.ENOENT();
        GridFile file = adaptor.get(path, token.token);
        if (file == null) return -ErrorCodes.ENOENT();
        if (file.getMetadata().get("isDir") != null) return -ErrorCodes.EISDIR();
        try {
            return adaptor.read(buf, path, size, offset, token.token);
        }catch (BufferSizeTooLargeException e1){
            return -ErrorCodes.EINVAL();
        }catch (OutOfRangeException e2){
            return -ErrorCodes.EIO();
        }catch (NotFoundException e3){
            return -ErrorCodes.ENOENT();
        }catch (NotReadyException e4){
            return -ErrorCodes.EBADF();
        }
    }

    @Override
    public int write(String path, Pointer buf, @size_t long size, @off_t long offset, FuseFileInfo fi) {
        if("/config.properties".equals(path)){
            byte[] content = new byte[(int)size-2];
            buf.get(2,content,0,(int)size-2);
            String contentString = new String(content);
            System.out.println("start");
            System.out.println("<"+contentString+">");
            System.out.println("end");
            return 0;
        }
        TokenBucket token = getToken(path);
        if(token == null ) return -ErrorCodes.ENOENT();
        GridFile file = adaptor.get(path, token.token);
        if (file == null) return -ErrorCodes.ENOENT();
        if (file.getMetadata().get("isDir") != null) return -ErrorCodes.EISDIR();
        try {
            return adaptor.write(buf, path, offset, size, token.token);
        }catch (OutOfRangeException e2){
            return -ErrorCodes.EIO();
        }catch (NotFoundException e3){
            return -ErrorCodes.ENOENT();
        }
    }

    @Override
    public int readdir(String path, Pointer buf, FuseFillDir filter, @off_t long offset, FuseFileInfo fi) {
        filter.apply(buf, ".", null, 0);
        filter.apply(buf, "..", null, 0);
        if ("/".equals(path)) {
            FuseContext ctx = getContext();
            List<TokenBucket> bucketList = Optional.ofNullable(pidToBucketList.get(ctx.pid.get())).orElse(defaultBucket);
            for(TokenBucket b : bucketList)
                filter.apply(buf, b.context.substring(1), null, 0);
            return 0;
        }
        TokenBucket token = getToken(path);
        if(token == null) return -ErrorCodes.ENOENT();
        if(!token.context.equals(path)){
            GridFile file = adaptor.get(path, token.token);
            if (file == null) return -ErrorCodes.ENOENT();
            if (file.getMetadata().get("isFile") != null) return -ErrorCodes.ENOTDIR();
        }
        for(String s : adaptor.readdir(path, buf, filter, token.token))
            filter.apply(buf, s, null, 0);
        return 0;
    }


    @Override
    public int statfs(String path, Statvfs stbuf) {
        if (Platform.getNativePlatform().getOS() == WINDOWS) {
            // statfs needs to be implemented on Windows in order to allow for copying
            // data from other devices because winfsp calculates the volume size based
            // on the statvfs call.
            // see https://github.com/billziss-gh/winfsp/blob/14e6b402fe3360fdebcc78868de8df27622b565f/src/dll/fuse/fuse_intf.c#L654
            if ("/".equals(path)) {
                stbuf.f_blocks.set(1024 * 1024); // total data blocks in file system
                stbuf.f_frsize.set(255 * 1024);        // fs block size
                stbuf.f_bfree.set(1024 * 1024);  // free blocks in fs
            }
        }
        return super.statfs(path, stbuf);
    }

    @Override
    public int rename(String path, String newName) {
        TokenBucket token = getToken(path);
        if(token == null) return -ErrorCodes.ENOENT();
        GridFile file = adaptor.get(path, token.token);
        if (file == null) return -ErrorCodes.ENOENT();
        if (file.getMetadata().get("isDir") != null) return -ErrorCodes.EISDIR();
        adaptor.rename(path, newName, token.token);
        return 0;
    }

    @Override
    public int rmdir(String path) {
        if ("/".equals(path)) return -ErrorCodes.ENOENT();
        TokenBucket token = getToken(path);
        if(token == null || token.context.equals(path)) return -ErrorCodes.ENOENT();

        GridFile file = adaptor.get(path, token.token);
        if (file == null) return -ErrorCodes.ENOENT();
        if (file.getMetadata().get("isFile") != null) return -ErrorCodes.ENOTDIR();
        adaptor.delete(path, token.token);
        return 0;
    }

    @Override
    public int truncate(String path, long offset) {
        if("/config.properties".equals(path)){
            return 0;
        }
        TokenBucket token = getToken(path);
        if(token == null) return -ErrorCodes.ENOENT();
        GridFile file = adaptor.get(path, token.token);
        if (file == null) return -ErrorCodes.ENOENT();
        if (file.getMetadata().get("isDir") != null) return -ErrorCodes.EISDIR();
        adaptor.truncate(file.getId(), offset, token.token);
        return 0;
    }

    @Override
    public int unlink(String path) {
        if ("/".equals(path)) return -ErrorCodes.ENOENT();
        TokenBucket token = getToken(path);
        if(token == null || token.context.equals(path)) return -ErrorCodes.ENOENT();

        GridFile file = adaptor.get(path, token.token);
        if (file == null) return -ErrorCodes.ENOENT();
        if (file.getMetadata().get("isDir") != null) return -ErrorCodes.EISDIR();
        adaptor.delete(path, token.token);
        return 0;
    }


    public static class TokenBucket{
        public String token;
        public String context;
        TokenBucket(String token, String context){
            this.token = token;
            this.context = context;
        };
    }
}
